/*
 * Copyright 2008-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baturu.mybatisminus.base;

import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * copy from spring data jpa
 */
public class Sort implements Serializable {

    private static final long serialVersionUID = -2828836502497191532L;

    private static final Sort UNSORTED = Sort.by(new Order[0]);

	private final List<Order> orders;

    public static final Direction DEFAULT_DIRECTION = Direction.ASC;

    private static final Pattern PROPERTY_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$");


	public List<Order> getOrders() {
		return orders;
	}

	/**
	 * Creates a new {@link Sort} instance.
	 * 
	 * @param direction defaults to {@link Sort#DEFAULT_DIRECTION} (for {@literal null} cases, too)
	 * @param properties must not be {@literal null}, empty or contain {@literal null} or empty strings.
	 */
	public Sort(Direction direction, String... properties) {
		this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties));
	}

	/**
	 * Creates a new {@link Sort} instance.
	 * 
	 * @param direction defaults to {@link Sort#DEFAULT_DIRECTION} (for {@literal null} cases, too)
	 * @param properties must not be {@literal null} or contain {@literal null} or empty strings.
	 */
	public Sort(Direction direction, List<String> properties) {
	    if(direction == null) {
	        direction = DEFAULT_DIRECTION;
        }
		if (properties == null || properties.isEmpty()) {
			this.orders = new ArrayList<>();
			return;
		}

		this.orders = new ArrayList<>(properties.size());

		for (String property : properties) {
		    if(isLegal(property)) {
                this.orders.add(new Order(direction, property));
            }
		}
	}

	/**
	 * Creates a new {@link Sort} for the given properties.
	 * 
	 * @param properties must not be {@literal null}.
	 * @return
	 */
	public static Sort by(String... properties) {
        if(properties == null) {
            return UNSORTED;
        }
		return properties.length == 0 ? Sort.unsorted() : new Sort(properties);
	}

    private Sort(String... properties) {
        this(DEFAULT_DIRECTION, properties);
    }


    /**
	 * Creates a new {@link Sort} for the given {@link Order}s.
	 * 
	 * @param orders must not be {@literal null}.
	 * @return
	 */
	public static Sort by(List<Order> orders) {
		return orders == null || orders.isEmpty() ? Sort.unsorted() : new Sort(orders);
	}

    private Sort(List<Order> orders) {
        if(orders == null || orders.isEmpty()) {
            this.orders = new ArrayList<>();
            return;
        }
        this.orders = Collections.unmodifiableList(orders);
    }

	/**
	 * Creates a new {@link Sort} for the given {@link Order}s.
	 * 
	 * @param orders must not be {@literal null}.
	 * @return
	 */
	public static Sort by(Order... orders) {
	    if(orders == null) {
	        return UNSORTED;
        }
		return new Sort(orders);
	}

    private Sort(Order... orders) {
        this(Arrays.asList(orders));
    }

	/**
	 * Creates a new {@link Sort} for the given {@link Order}s.
	 * 
	 * @param direction must not be {@literal null}.
	 * @param properties must not be {@literal null}.
	 * @return
	 */
	public static Sort by(Direction direction, String... properties) {
        if(properties == null || properties.length == 0) {
            return UNSORTED;
        }
        if(direction == null) {
            direction = DEFAULT_DIRECTION;
        }
        Direction finalDirection = direction;
        return Sort.by(Arrays.stream(properties)//
				.filter(Sort::isLegal).map(it -> new Order(finalDirection, it))//
				.collect(Collectors.toList()));
	}

	/**
	 * Returns a {@link Sort} instances representing no sorting setup at all.
	 * 
	 * @return
	 */
	public static Sort unsorted() {
		return UNSORTED;
	}

	/**
	 * 将已有的所有排序字段改成降序
	 * @return
	 */
	public Sort descending() {
		return withDirection(Direction.DESC);
	}

	/**
	 * 将已有的所有排序字段改成升序
	 * 
	 * @return
	 */
	public Sort ascending() {
		return withDirection(Direction.ASC);
	}

	public boolean isSorted() {
		return !orders.isEmpty();
	}

	public boolean isUnsorted() {
		return !isSorted();
	}


	/*
	 * (non-Javadoc)
	 * @see java.lang.Iterable#iterator()
	 */
	public Iterator<Order> iterator() {
		return this.orders.iterator();
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {

		if (this == obj) {
			return true;
		}

		if (!(obj instanceof Sort)) {
			return false;
		}

		Sort that = (Sort) obj;

		return this.orders.equals(that.orders);
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {

		int result = 17;
		result = 31 * result + orders.hashCode();
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return orders.isEmpty() ? "UNSORTED" : StringUtils.collectionToCommaDelimitedString(orders);
	}

	/**
	 * Creates a new {@link Sort} with the current setup but the given order direction.
	 * 
	 * @param direction
	 * @return
	 */
	private Sort withDirection(Direction direction) {

		return Sort.by(orders.stream().map(it -> new Order(direction, it.getProperty())).collect(Collectors.toList()));
	}

	private static boolean isLegal(String property) {
	    if(StringUtils.isEmpty(property)) {
	        return false;
        }
	    Matcher matcher = PROPERTY_PATTERN.matcher(property);
	    return matcher.matches();
    }

	/**
	 * Enumeration for sort directions.
	 * 
	 * @author Oliver Gierke
	 */
	public static enum Direction {

		ASC, DESC;

		/**
		 * Returns whether the direction is ascending.
		 * 
		 * @return
		 * @since 1.13
		 */
		public boolean isAscending() {
			return this.equals(ASC);
		}

		/**
		 * Returns whether the direction is descending.
		 * 
		 * @return
		 * @since 1.13
		 */
		public boolean isDescending() {
			return this.equals(DESC);
		}

		/**
		 * Returns the {@link Direction} enum for the given {@link String} value.
		 * 
		 * @param value
		 * @throws IllegalArgumentException in case the given value cannot be parsed into an enum value.
		 * @return
		 */
		public static Direction fromString(String value) {

			try {
				return Direction.valueOf(value.toUpperCase(Locale.US));
			} catch (Exception e) {
				throw new IllegalArgumentException(String.format(
						"Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).", value), e);
			}
		}

        public static Optional<Direction> fromOptionalString(String value) {

            try {
                return Optional.of(fromString(value));
            } catch (IllegalArgumentException e) {
                return Optional.empty();
            }
        }

	}

	public String getOrderBy() {
	    if(CollectionUtils.isEmpty(orders)) {
	        return null;
        }
        StringBuilder sb = new StringBuilder(" order by ");
	    for(Order order : orders) {
	        sb.append("`").append(order.getProperty()).append("`").append(" ").append(order.isDescending() ? "desc": "asc").append(",");
        }
        sb.setLength(sb.length() - 1);
	    return sb.toString();
    }

	/**
	 * PropertyPath implements the pairing of an {@link Direction} and a property. It is used to provide input for
	 * {@link Sort}
	 * 
	 * @author Oliver Gierke
	 * @author Kevin Raymond
	 */

	public static class Order implements Serializable {

		private final String property;
        private final Direction direction;


		/**
		 * Creates a new {@link Order} instance. Takes a single property. Direction defaults to
		 * {@link Sort#DEFAULT_DIRECTION}.
		 * @param property must not be {@literal null} or empty.
		 * @since 2.0
		 */
		public static Order by(String property) {
			return new Order(DEFAULT_DIRECTION, property);
		}

		/**
		 * Creates a new {@link Order} instance. Takes a single property. Direction is {@link Direction#ASC} and
		 *
		 * @param property must not be {@literal null} or empty.
		 * @since 2.0
		 */
		public static Order asc(String property) {
			return new Order(Direction.ASC, property);
		}

		/**
		 * Creates a new {@link Order} instance. Takes a single property. Direction is {@link Direction#ASC} and
		 *
		 * @param property must not be {@literal null} or empty.
		 * @since 2.0
		 */
		public static Order desc(String property) {
			return new Order(Direction.DESC, property);
		}

		/**
		 * Creates a new {@link Order} instance. if order is {@literal null} then order defaults to
		 * {@link Sort#DEFAULT_DIRECTION}
		 * 
		 * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}
		 * @param property must not be {@literal null} or empty.
		 * @since 1.7
		 */
		public Order(Direction direction, String property) {

			if (!StringUtils.hasText(property)) {
				throw new IllegalArgumentException("Property must not null or empty!");
			}
			if(!isLegal(property)) {
			    throw new IllegalArgumentException("Property name is illegal!");
            }
			this.direction = direction == null ? DEFAULT_DIRECTION : direction;
			this.property = property;
		}

		/**
		 * Returns the order the property shall be sorted for.
		 * 
		 * @return
		 */
		public Direction getDirection() {
			return direction;
		}

		/**
		 * Returns the property to order for.
		 * 
		 * @return
		 */
		public String getProperty() {
			return property;
		}

		/**
		 * Returns whether sorting for this property shall be ascending.
		 * 
		 * @return
		 */
		public boolean isAscending() {
			return this.direction.isAscending();
		}

		/**
		 * Returns whether sorting for this property shall be descending.
		 * 
		 * @return
		 * @since 1.13
		 */
		public boolean isDescending() {
			return this.direction.isDescending();
		}

		/**
		 * Returns a new {@link Order} with the given {@link Direction}.
		 * 
		 * @param direction
		 * @return
		 */
		public Order with(Direction direction) {
			return new Order(direction, this.property);
		}

		/**
		 * Returns a new {@link Order}
		 * 
		 * @param property must not be {@literal null} or empty.
		 * @return
		 * @since 1.13
		 */
		public Order withProperty(String property) {
			return new Order(this.direction, property);
		}

		/**
		 * Returns a new {@link Sort} instance for the given properties.
		 * 
		 * @param properties
		 * @return
		 */
		public Sort withProperties(String... properties) {
			return Sort.by(this.direction, properties);
		}


		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {

			int result = 17;

			result = 31 * result + direction.hashCode();
			result = 31 * result + property.hashCode();
			return result;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {

			if (this == obj) {
				return true;
			}

			if (!(obj instanceof Order)) {
				return false;
			}

			Order that = (Order) obj;

			return this.direction.equals(that.direction) && this.property.equals(that.property);
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
            return String.format("%s: %s", property, direction);
		}
	}
}
